Join 運算子,是 LINQ 標準查詢運算子中,應用難度較高的運算子,我盡量用範例引導的方式讓大家熟悉它。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
Join 運算子,可以讓我們把兩個來源序列聚合為一個輸出序列。Join 運算子有兩個多載方法,但是第二個多載方法 LINQ to Entities 和 LINQ to SQL 並不支援,使用時請注意:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector
)
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector,
IEqualityComparer<TKey> comparer
)
Join 運算子的邏輯,大致上是這樣:
Join 運算子有幾個要注意的地方:
在看範例程式碼前,我們要先建立兩個資料來源序列,以利後續 Demo,為了方便大家使用,所以採用 LINQ to Objects 的方式處理,做資料來源的程式碼如下:
void Main()
{
var Customers = DataProvider.getCustomers();
var Orders = DataProvider.getOrders();
Customers.Dump();
Orders.Dump();
}
public static class DataProvider
{
public static List<Customer> getCustomers()
{
var Customers = new List<Customer>
{
new Customer {ID = 1, Name = "Leo"},
new Customer {ID = 2, Name = "Rose"},
new Customer {ID = 3, Name = "Alvin"},
new Customer {ID = 4, Name = "Emy"},
new Customer {ID = 5, Name = "Alice"},
new Customer {ID = 6, Name = "Bobo"}
};
return Customers;
}
public static List<Order> getOrders()
{
var Orders = new List<Order>
{
new Order {ID = 1, CustomerID = 1, Date = new DateTime(2012,1,5), Description = "Mouse", Price = 480},
new Order {ID = 2, CustomerID = 1, Date = new DateTime(2012,2,15), Description = "Books", Price = 880},
new Order {ID = 3, CustomerID = 2, Date = new DateTime(2011,6,16), Description = "Keyboard", Price = 290},
new Order {ID = 4, CustomerID = 2, Date = new DateTime(2012,3,25), Description = "NoteBook", Price = 16800},
new Order {ID = 5, CustomerID = 3, Date = new DateTime(2012,8,15), Description = "Mouse", Price = 480},
new Order {ID = 6, CustomerID = 4, Date = new DateTime(2011,6,22), Description = "NoteBook", Price = 16800},
new Order {ID = 7, CustomerID = 4, Date = new DateTime(2011,10,10), Description = "Mouse", Price = 480},
new Order {ID = 8, CustomerID = 4, Date = new DateTime(2012,9,8), Description = "Camera", Price = 29900},
};
return Orders;
}
}
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
}
public class Order
{
public int ID { get; set; }
public int CustomerID { get; set; }
public DateTime Date { get; set; }
public string Description { get; set; }
public Decimal Price { get; set; }
}
準備好資料來源後,我們來看最簡單的 Join 範例:
//請注意:建立資料來源的程式碼就不重覆貼了,請自行參閱前述內容
void Main()
{
var Customers = DataProvider.getCustomers();
var Orders = DataProvider.getOrders();
var query = from c in Customers
join o in Orders on c.ID equals o.CustomerID
select c.Name + " 買了 " + o.Description;
query.Dump();
// Customers.Join (
// Orders,
// c => c.ID,
// o => o.CustomerID,
// (c, o) => ((c.Name + " 買了 ") + o.Description)
// ).Dump();
}
Join 運算子,在查詢表達式中,必須搭配 in、on 和 equals 三個關鍵字使用。上述範例註解的部分是對等的方法架構查詢語法。範例程式碼說明如下:
這個範例請務必了解,因為這是最簡單的版本,也是基礎樣式,若不能融會貫通,後續的範例就更難理解了。
接著我們要研究一個小題目,就是 Join 運算子和之前學過的 SelectMany 運算子,大多時候可以互相替代,以上述範例來看,可改寫成以下 SelectMany 運算子的樣式:
void Main()
{
var Customers = DataProvider.getCustomers();
var Orders = DataProvider.getOrders();
// var query = from c in Customers
// join o in Orders on c.ID equals o.CustomerID
// select c.Name + " 買了 " + o.Description;
var query =
from c in Customers
from o in Orders where c.ID == o.CustomerID
select c.Name + " 買了 " + o.Description;
query.Dump();
// Customers.SelectMany(
// c => Orders,
// (c, o) => new {c = c, o = o})
// .Where (temp0 => (temp0.c.ID == temp0.o.CustomerID))
// .Select (temp0 => ((temp0.c.Name + " 買了 ") + temp0.o.Description))
// .Dump();
}
上述沒有註解的查詢表達式,用了兩個 from,並透過第二個 from 的 where 關鍵子指定相等聯結的欄位,此語法轉譯為方法架構查詢,就是後面註解起來的區塊。即然可以替代,那誰比較好呢?這問題沒有正確答案,看個人喜好就行,不過至少可以研究看看,那一種方式效能會比較好呢?
就我個人實務應用的經驗,都是 Join 比較快,當然我們要有實驗精神,所以我把上述的範例延伸為 Join v.s. SelectMany 的效能比賽,範例程式如下:
void Main()
{
var Customers = DataProvider.getCustomers();
var Orders = DataProvider.getOrders();
Console.WriteLine("Customer 數量:" + Customers.Count());
Console.WriteLine("Orders 數量:" + Orders.Count());
Stopwatch sw = new Stopwatch();
sw.Start();
var slowQuery =
from c in Customers
from o in Orders where c.ID == o.CustomerID
select c.Name + " 買了 " + o.Description;
Console.WriteLine("Slow query result count: " + slowQuery.Count());
sw.Stop();
Console.WriteLine("Slow query with SelectMany (Milliseconds): " + (sw.ElapsedMilliseconds));
sw.Restart();
var fastQuery =
from c in Customers
join o in Orders on c.ID equals o.CustomerID
select c.Name + " 買了 " + o.Description;
Console.WriteLine("Fast query result count: " + fastQuery.Count());
sw.Stop();
Console.WriteLine("Fast query with Join (Milliseconds): " + (sw.ElapsedMilliseconds));
}
/* 輸出:
Customer 數量:10000
Orders 數量:12000
Slow query result count: 10008
Slow query with SelectMany (Milliseconds): 16199
Fast query result count: 10008
Fast query with Join (Milliseconds): 9
*/
為了效能測試,我微調了產生 Customers、Orders 的程式,一次可產出如上圖 10,000 和 12,000 筆資料,且有 10,008 筆資料可以聯結起來(故意不做完全對應),最後輸出兩種運算子的查詢結果筆數及所花費的時間,證明相同的查詢結果,Join 運算子在大量資料查詢時,效能遠勝 SelectMany 運算子。
其實這個結果要說明差異點很簡單,從兩種運算子的方法架構查詢語法即可一窺究竟:
Customers.SelectMany (c => Orders, (c, o) => new {c = c, o = o})
.Where (temp0 => (temp0.c.ID == temp0.o.CustomerID))
.Select (temp0 => ((temp0.c.Name + " 買了 ") + temp0.o.Description))
Customers.Join (
Orders,
c => c.ID,
o => o.CustomerID,
(c, o) => ((c.Name + " 買了 ") + o.Description)
)
最重要的差異就是,SelectMany 運算子一開始要先把 Customers 和 Orders 中所有資料全部展開,產生新的 IEnumerable<匿名型別> 序列,然後才進行 Where 條件過濾,最後才整理出要回傳的 TResult。也就是說,為了取回我們要的結果,它必須產生三個 IEnumerable 序列才能得到結果,記憶體的消耗極大,當然就快不起來。
Join 運算子不一樣,如同文章開頭的敘述,它一開始先列舉 inner 中所有項目,將主鍵存到 Hashtable 中,然後逐一列舉 outer 中的項目,拿 outer 的主鍵到 Hashtable 中對應,有符合就整理成要輸出的匿名型別資料,並指定到 IEnumerable<TResult> 中。兩種運算子執行作業的方式大不相同,當然也就造成效能表現上的不同。